인터페이스는 서비스 공급자와 자신의 객체를 이 서비스에 사용하고 싶은 클래스 간의 계약을 기술하는 메커니즘이다.
아래와 같이 기본 구현을 작성하지 않고 선언만 한 메서드를 추상(abstract) 메서드라고 한다. 인터페이스의 메서드로도 다른 메서드를 구현할 수 있다.
public interface IntSequence {
boolean hasNext();
int next();
}
public static double average(IntSequence seq, int n) {
int count = 0;
double sum = 0;
while (seq.hasNext() && count < n) {
count++;
sum += seq.next();
}
return count == 0 ? 0 : sum / count;
}
인터페이스에서 작성된 추상 메서드는 구상 클래스에서 구현된다.
public class SquareSequence implements IntSequence {
public int i;
@Override
public boolean hasNext() {
return true;
}
@Override
public int next() {
i++;
return i;
}
}
public static void main(String[] args) {
SquareSequence squareSequence = new SquareSequence();
double avg = average(squareSequence, 100);
}
객체의 클래스가 인터페이스를 구현할 때는 이 객체를 해당 인터페이스 타입 변수에 할당할 수 있다. 또 이 객체를 해당 인터페이스를 기대하는 메서드에도 전달할 수 있다.
IntSequence digits = new DigitSequence(1729);
double avg = average(digits, 100);
유용한 용어를 하나 알아두자. T타입의 모든 값을 변환 없이 S 타입의 변수에 할당할 수 있다면 S타입은 T타입(subtype)의 supertype이다.
인터페이스 타입으로 변수를 선언할 수 있지만, 타입이 인터페이스 자체인 객체는 만들 수 없다. 모든 객체는 클래스의 인스턴스다.
가끔 반대로 변환(S -> T)해야 할 때도 있다. 이때는 cast(강제변환)를 사용해야한다. 밑의 코드에서 IntSequence 변수에 저장된 객체가 실제로는 DigitSequence 라는 사실을 안다면 다음과 같이 타입 변환이 가능하다.
IntSequence sequence = new DigitSequence(10);
DigitSequence digits2 = (DigitSequence) sequence;
위 코드에선 안보이지만, rest() method 는 DigitSequence의 메서드이다. 하지만 IntSequence로는 rest를 사용할 수 없으니 DigitSequence로 캐스트 하는것이 필요하다.
하지만 객체는 실제 클래스나 그 슈퍼타입으로만 캐스트할 수 있으며 잘못 캐스트 하면 컴파일 시간 오류나 클래스 캐스트 예외(ClassCastException)가 일어난다.
//컴파일 타임에 에러가 잡힌다.
String digitString = (String) sequence;
//밑의 클래스는 IntSequence를 상속받지 않은 클래스를 변수로 선언한 후에, IntSequence를 상속받은 객체를 캐스트하고 있다.
RandomSequence randoms = (RandomSequence) sequence; //ClassCastException을 던진다.
예외가 일어나지 않게 하기 위해 객체가 원하는 타입인지 instanceof 연산자로 검사해야한다.
if (sequence instanceof DigitSequence) {
DigitSequence digitSequence = (DigitSequence) sequence;
}
instanceof 연산자는 null에 안전하다.(obj가 null이면 instance of Type 표현식은 false로 나온다.) 결국 null은 어떤 타입의 객체도 참조할 수 없다.
인터페이스는 또 다른 인터페이스를 확장(extend)해서 원래 있던 메서드 외의 추가 메서드를 요구하거나 제공할 수 있다. 그 상황에서 구현 클래스는 두 메서드 모두 구현해야 하며 두 인터페이스 타입 중 어느 것으로 해당 클래스의 객체를 변환할 수 있다.
public interface Closeable {
void close();
}
public interface Channel extends Closeable {
boolean isOpen();
}
public class ImpleChannel implements Channel {
@Override
public boolean isOpen() {
return false;
}
@Override
public void close() {
}
}
클래스는 인터페이스를 몇 개든 구현할 수 있다. 밑과 같이 구현하면 IntSequence와 Closeable을 슈퍼타입으로 둔다.
public class MultiImplementation implements IntSequence, Closeable {
@Override
public void close() {
}
@Override
public boolean hasNext() {
return false;
}
@Override
public int next() {
return 0;
}
}
인터페이스에 정의한 변수는 자동으로 public static final이 된다. 또 이 상수들은 구현 클래스가 SwingConstants 인터페이스를 구현한다면 SwingConstants 한정어(qualifier)를 생략하고 간단히 사용할 수 있다.
public interface SwingConstants {
int NORTH = 1;
int NORTH_EAST = 2;
int EAST = 3;
}
public class Swing implements SwingConstants {
public void useFinalInInterface() {
System.out.println(NORTH_EAST + NORTH);
}
}
자바 8 이전에는 인터페이스의 모든 메서드가 추상 메서드였지만, 자바 9에서는 실제 구현이 있는 메서드 세 종류(정적 메서드, 기본 메서드, 비공개 메서드)를 인터페이스에 추가할 수 있다. 하지만 비공개 메서드는 private를 넣을때 컴파일 에러가 나는것을 볼 수 있다.
기술적으로 보면 인터페이스에 정적 메서드를 넣지 못할 이유는 없었지만, 인터페이스를 추상 명세로 보는 관점에는 맞지 않았다. 하지만 이러 사고가 진화했으며 팩터리메서드에 아주 잘 맞는다.
밑의 메서드는 인터페이스를 구현한 클래스의 인스턴스를 돌려주지만, 호출자는 이 인스턴스가 어느 클래스의 인스턴스인지 신경 쓸 필요없다.
IntSequence digits = IntSequence.digitsOf(1729);
static interface IntSequence(int n) {
return new DigitSequence(n);
}
인터페이스에 있는 어느 메서드에서든 기본(default) 구현을 작성할 수 있다. 기본 메서드에는 반드시 default 제어자를 붙여야 한다.
public interface IntSequence {
default boolean next() {
return true;
}
}
기본 메서드의 주요 용도는 인터페이스를 진화(interface evolution) 시키는 것이다. 이전 부터 있던 Collection 인터페이스에 자바 8 부터 stream 메서드를 추가 했다. 우리는 stream이 default가 아니라고 가정해보자. 그러면 Bag 클래스는 새로 추가된 메서드를 구현하지 않으므로 컴파일 되지 않는다. 인터페이스에 기본 메서드가 아닌 메서드를 추가하면 소스 수준 에서 호환(source Comparatible)되지 않는다.
근데 클래스를 다시 컴파일 하지 않고 Bag 클래스가 포함된 JAR 파일을 그대로 사용한다고 하자. 빠진 메서드가 있는데도 잘 로드된다. 프로그램 에서 여전히 Bag 인스턴스를 생성할 수 있고 문제가 없다.(인터페이스에 메서드를 추가하는 것은 바이너리수준에서 호환(binary-compatible)된다.) 하지만 하지만 프로그램에서 Bag 인스턴스로 stream 메서드를 호출하면 AbstractMethodError 가 일어난다. 메서드를 default로 선언하면 이 문제를 해결할 수 있다. 즉, Bag 클래스가 제대로 컴파일 된다. 또 Bag 클래스를 다시 컴파일 하지 않고 로드한 후 Bag 인스턴스로 stream 메서드를 호출하면 Collection.stream 메서드가 호출된다. 이부분에 대해선 나중에 코드로 실험해보자.
클래스가 인터페이스를 두 개 구현한다고 하자. 그런데 한 인터페이스에는 기본 메서드가 있고, 다른 한 인터페이스에는 이 메서드와 이름, 매개변수 타입이 같은 메서드(default로 선언 되있던 아니던)가 있다면 반드시 충돌을 해결해야한다.
밑의 코드는 충돌이 일어나는 코드이다. Person과 Identified 인터페이스에서 getId 메서드를 동시에 상속 받는데 컴파일러는 그중 하나를 선택하지 못하므로 프로그래머가 처리해야한다.
public interface Person {
String getName();
default int getId() {return 0;}
}
public interface Identified {
default int getId() { return Math.abs(hashCode());}
}
public class Employee implements Person, Identified{
@Override
public String getName() {
return null;
}
//Employee 클래스에 getId 메서드를 추가한 후 고유의 id 체계를 구현
// @Override
// public int getId() {
// return 0;
// }
// 또는 충돌한 메서드중 하나에 위임.
// public int getId() { return Identified.super.getId();} //super 키워드로 슈퍼타입의 메서드를 호출 가능.
}
위의 문제를 해결하기 위해선 Employee 클래스에 getId 메서드를 추가한 후 고유의 ID 체계를 구현하거나, 가장 밑의 코드처럼 충돌한 메서드 중 하나에 위임해야한다.
public class Employee implements Person, Identified{
@Override
public String getName() {
return null;
}
//Employee 클래스에 getId 메서드를 추가한 후 고유의 id 체계를 구현
@Override
public int getId() {
return 0;
}
//또는 충돌한 메서드중 하나에 위임.
public int getId() { return Identified.super.getId();} //super 키워드로 슈퍼타입의 메서드를 호출 가능.
}
super 키워드로 슈퍼타입의 메서드를 호출할 수 있다.
이번에는 Identified 인터페이스에 getId를 기본 메서드로 구현하지 않는다고 하자. 우리는 컴파일러가 default를 선택할거라고 생각하겠지만 컴파일러는 컴파일 에러를 보고하며, 위에서 본것과 같이 둘중 하나로 선택해야한다.
public interface Identified {
int getId();
}
만약 두 인터페이스 모두 공유 메서드의 default를 제공하지 않는다면 충돌이 일어나지 않지만, 구현하는 클래스에 메서드를 구현해야하고 그렇지 않을 경우엔 abstract 를 붙혀줘야한다.
클래스가 슈퍼클래스를 확장(상속)하고 인터페이스를 구현해서 두 인터페이스가 모두 같은 메서드를 상속받을 때는 슈퍼클래스의 메서드만 중요하고 인터페이스의 기본 메서드는 무시된다.
public class PersonClass {
public String test() {
return "클래스";
}
}
public interface Identified {
default int getId() { return Math.abs(hashCode());}
default String test() { return "인터페이스의 기본 메서드";}
}
public class TestExtend extends PersonClass implements Identified {
}
TestExtend testExtend = new TestExtend();
System.out.println(testExtend.test()); //인터페이스가 아닌 클래스에 정의된 메서드를 사용함.
자바 9 부터 인터페이스에 비공개 메서드를 만들 수 있는데, 비공개 메서드는 static이나 인스턴스 메서드(인스턴스를 생성과 동시에 메서드를 초기화 하는 것)는 될 수 있지만, default(오버라이드가 가능하기 때문에) 메서드는 될 수 없다. 하지만 코드로 직접 실행해 보면서 interface 내에서 private 로 선언은 불가능하다. 다시 찾아 봐야겠다.
public interface PrivateMethod {
private static void makeFiniteSequence(int... value) { System.out.println(value);}
static void of(int a) {
makeFiniteSequence(1);
}
static void of(int a, int b) {
makeFiniteSequence(2);
}
static void of(int a, int b,int c) {
makeFiniteSequence(3);
}
}
인터페이스는 그저 클래스가 구현하기로 약속한 메서드 집합이다.
어떤 객체를 정렬하기 위해선 해당 클래스가 Comparable 인터페이스를 구현 해야한다. 항상 id가 양의 정수라면 위의 메서드를 사용해도 되지만, 정확한 값을 얻기 위해 Double에서 제공하는 compare 메서드를 이용한다.
public interface Comparable<T> {
int compareTo(T other);
}
@Override
public int compareTo(Employee2 other) {
return getId() - other.getId(); //id가 항상 0 이상이라면 잘 작동함.
}
@Override
public int compareTo(Employee2 other) {
return Double.compare(getSalary(),other.getSalary());
}
String 클래스는 자바 라이브러리에 들어 있는 100개 이상의 다른 클래스와 마찬가지로 Comparable 인터페이스를 구현한다. Arrays.sort 메서드는 Comparable 객체로 구성된 배열을 정렬한다.
String[] friends = {"B", "C", "A", "Z", "F", "C"};
Arrays.sort(friends);
Arrays.sort 메서드는 컴파일 시간에 인수가 Comparable 객체의 배열인지 검사하지 않고, Comparable 인터페이스를 구현하지 않은 클래스 요소를 만나면 예외를 던진다. 밑의 코드가 예시 코드이다.
public class Employee implements Person{
@Override
public String getName() {
return null;
}
@Override
public int getId() {
return 0;
}
}
Employee[] employees = {new Employee(), new Employee()};
Arrays.sort(employees); //컴파일 에러가 나지는 않지만, 실행시 에러 반환.
String 클래스는 compareTo 메서드를 두가지 방법으로 구현하지 못한다. 또 String 클래스는 우리가 소유한 클래스가 아니기 때문에 수정도 불가능하다. 이런 상황을 다루기 위해 Arrays.sort 메서드에는 다른 버전이 존재하고 이 버전에선 비교자(comparator)를 매개변수로 받는다.(비교자는Comparator 인터페이스를 구현하는 클래스의 인스턴스 이다.)
compare 메서드는 문자열(String.compareTo(다른 string변수))과 다르게 비교자 객체로 호출한다.
public interface Comparator<T> {
int compare(T first, T second);
}
public class LengthComparator implements Comparator<String> {
@Override
public int compare(String first, String second) {
return first.length() - second.length();
}
}
배열을 정렬 하려면 LengthComparator 객체를 Arrays.sort 메서드에 전달해야한다. 하지만 밑의 코드는 컴파일 에러가 난다. IDE 에서는 LengthComparator가 Comparator를 구현하고 있고, sort 에서 두번째 매개변수로 Comparator 타입을 변수를 받고있는데 LengthComparator로 받아 컴파일 에러가 뜨고 있다. 왜 그런지는 모르겠다.. 이후에 찾아봐야겠다.
Arrays.sort(friends2, new LengthComparator());
특정 작업을 별도의 스레드에서 수행하거나 실행횽 스레드 풀에 넣기 위해 task를 정의하는데 이를 위해 Runnable 인터페이스를 구현해야 한다. Runnable 인터페이스에는 메서드가 한 개만 있다.
public class HelloTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i ++) {
System.out.println("Hello, World");
}
}
}
Runnable task = new HelloTask();
Thread thread = new Thread(task);
thread.start();
사용자가 버튼을 클릭하거나, 메뉴 옵션을 선택하거나, 슬라이더를 드래그하거나 했을 때 수행할 액션(action)을 지정해야한다. 이런 액션을 callback이라고 한다.
public interface EventHandler<T> {
void handle(T event);
}
public class CancelAction implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
System.out.println("Oh noes!");
}
}
Button cancelButton = new Button("Cancel");
cancelButton.setOnAction();
람다 표현식(lamda expression)은 나중에 한 번 이상 실행할 수 있게 전달하는 코드 블록이다.
자바는 거의 모든 것이 객체인 객체지향 언어다. 자바에는 함수 타입이 없다. 그 대신 객체(특정 인터페이스를 구현하는 클래스의 인스턴스)로 표현한다. 람다 표현식은 이런 인스턴스를 생성하는 아주 편리한 문법을 제공한다.
밑의 코드는 람다 표현식의 예시이다. 람다 표현식은 쉽게 말해 코드 블록으로, 해당 코드에 전달해야 하는 변수의 명세(specification)까지 갖춘 것이다. 자바는 타입 결합이 강한 언어이므로 타입도 명시해야한다.
(String first, String second) -> first.length() - second.length()
람다 표현식의 바디에서 표현식 하나로는 표현할 수 없는 계산을 수행한다면 메서드를 쓸 때처럼 작성하면 된다. 다음과 같이 {}로 감싸고 명시적인 return 문을 사용한다.
(String first, String second) -> {
int difference = first.length() - second.length();
if (difference < 0) return -1;
else if (difference > 0) return -1;
else return 0;
};
람다 표현식에 매개변수가 없으면 매개변수가 없는 메서드 처럼 빈 괄호를 붙여야한다.
Runnable task = () -> { for (int i = 0; i < 1000; i ++) doWork(); }
람다 표현식의 매개변수 타입을 추론할 수 있다면 다음과 같이 매개 변수 타입을 생략 가능하다.
EventHandler<ActionEvent> listener = event -> {
System.out.println("oh noes!");
};
람다 표현식의 결과 타입은 명시하지 않는다. 하지만 컴파일러는 람다 표현식 바디에서 결과 타입을 추론한 후 기대하는 타입과 일치하는지 검사한다. 밑의 표현식은 기대하는 결과가 int타입(또는 Integer, long, double 같은 호환 타입)인 문맥에서 사용할 수 있다.
(String first, String second) -> first.length() - second.length();
람다 표현식은 단일 추상 메서드(single abstract method)를 가진 인터페이스(즉, 추상 메서드가 한 개만 있는 인터페이스) 자리에 사용할 수 있다. 이런 인터페이스를 함수형 인터페이스(functional interface)라고 한다.
함수형 인터페이스로 변환하는 것을 알아보기 위해 Arrays.sort 메서드의 두번째 매개변수인 Comparator의 인스턴스를 보자.(comparator 인터페이스에는 메서드가 하나만 있다.)
Arrays.sort(words, (first, second) -> first.length() - second.length());
함수 리터럴을 지원하는 거의 모든 프로그래밍 언어에서 (String, String) -> int 처럼 함수 타입을 선언하고, 이 함수 타입으로 변수를 선언한 후 함수를 변수에 호출할 수 있다. 하지만 자바에서는 이 중 하나만 람다 표현식으로 표현할 수 있으며, 표현식을 함수형 인터페이스 타입 변수에 저장해서 해당 인터페이스의 인스턴스로 변환하는 것이다.
자바 API 에는 수많은 함수형 인터페이스가 있으며 그중 하나가 Predicate 인터페이스다. ArrayList 클래스에는 매개변수로 Predicate를 받는 removeIf 메서드가 있는데 Predicate는 람다 표현식을 전달받을 용도로 특별히 설계했다.
public interface Predicate<T> {
boolean test(T t);
// 추상 메서드가 하나라면 default 메서드나 비공개 메서드가 있더라도 함수형 인터페이스이다.
}
list.removeIf(e -> e == null);
다른 코드에 전달하려는 액션을 수행하는 메서드가 이미 있을 땐 메서드 참조(method reference) 용 특수 문법을 사용하며, 이것은 메서드를 호출하는 람다 표현식 보다 간결하다.
밑의 코드는 대 소문자 구분 없이 문자열을 정렬할때 메서드 참조로 변경하는 코드이다. String::compareToIgnoreCase는 람다 표현식(x, y) -> x.compareToIgnoreCase(y)에 대응하는 메서드 참조다.
Arrays.sort(strings, (x, y) -> x.compareToIgnoreCase(y));
Arrays.sort(strings, String::compareToIgnoreCase);
다른 예로 Objects.isNull(x)를 보자. 이 메서드는 단순히 x == null의 결과 값을 반환한다. 이때는 isNull 메서드의 존재가 의미 없어 보이지만, isNull은 메서드 표현식으로 사용하도록 설계된 메서드다. 다음 호출은 리스트에서 null을 모두 제거한다.
list.removeIf(Objects::isNull);
또 다른 예로 ArrayList 클래스에는 지정한 함수를 각 요소에 적용 하는 forEach 메서드가 있다. forEach는 람다를 매개변수로 받는다.
list.forEach(x -> System.out.println(x));
list.forEach(System.out::println);
:: 연산자는 이름과 메서드 이름을 분리하거나 객체의 이름과 메서드 이름을 분리한다.
Class::instanceMethod
Class::staticMethod
object::instanceMethod
같은 이름으로 오버로드된 메서드가 여러 개일 때 컴파일러는 어느 것을 의도했는지 문맥으로 알아내려 한다. 예를들어 println 메서드가 여러개 있다면 println은 ArrayList<String>의 forEach 메서드에 전달하면 println(String) 메서드가 전달된다.
메서드 참조에서 this 매개 변수를 캡처할 수 있다.
public class ReferThis {
public boolean equals(String a) {
System.out.println(a);
return true;
}
public void test() {
System.out.println("시작");
ArrayList<String> arr = new ArrayList<>();
arr.add("a");
arr.add("B");
arr.add("C");
arr.forEach(this::equals);
}
}
내부 클래스에서 EnclosingClass.this::methid로 자신을 감싸는 클래스의 this 참조를 캡처할 수 있다.
생성자 참조는 메서드 이름이 new라는 점만 제외하면 메서드와 같으며, 클래스에 생성자가 두 개 이상 있을 때는 문맥으로 어느 생성자를 호출할지 결정한다.
public class Employee implements Person, Identified{
String a;
public Employee(String a) {
this.a = a;
}
}
List<String> names = List.of("a","b","c","d");
Stream<Employee> stream = names.stream().map(Employee::new);
배열 타입으로도 생성자 참조를 만들 수 있다. int[]::new는 매개변수가 한 개(배열의 길이)인 생성자 참조다. 이 생성자 참조는 람다 표현식 n -> new int[n]과 같다.
배열 생성자 참조는 자바의 한게를 극복하는데 도움이 된다. 자바에서는 제네릭 타입으로 구성된 배열을 생성할 수 없다. Stream.toArray 같은 메서드는 요소 타입 배열이 아니라 Object 배열을 반환한다. 하지만 객체의 배열이 아니라 직원의 배열을 원하므로 다음과 같은 방법으로 해결한다.
Object[] employees = stream.toArray();
Employee[] buttons = stream.toArray(Employee[]::new);
람다를 사용하는 핵심 목적은 지연실행(deferred execution)이다. 결국 어떤 코드를 지금 당장 실행하고 싶다면 굳이 람다로 감쌀 필요 없이 그냥 실행하면 되기 때문이다. 다음은 코드를 나중에 실행하는 이유들이다.
밑의 코드는 repeat 메서드에서 반복 횟수와 액션을 람다로 받아 사용하는 코드이다. 이 예제에서는 간단히 Runnable 인터페이스를 사용하면 된다.
repeat(10, () -> System.out.println("Hello, World"));
public static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) action.run();
}
repeat(10, () -> System.out.println("Hello, World"));
몇번째 반복을 수행하는지 액션에 알리고 싶다면 int 매개변수를 받고 반환 타입을 void로 둔 함수형 인터페이스를 골라야한다. 우리는 IntConsumer 를 사용한다.
public interface IntConsumer {
void accept(int value);
}
public static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) action.accept(i);
}
repeat(10, i -> System.out.println("CountDown: "+ (9 - i)));
함수형 프로그래밍 언어는 대부분 함수 타입이 구조적(structural)이다. 문자열 두 개를 정수로 대응 시키는 함수를 만들 때는 Function2<String, String, Integer> 또는 (String, String) -> int 형태의 타입을 사용한다. 하지만 자바에서는 Comparator<String> 같은 함수형 인터페이스로 함수 의도를 선언한다. 프로그래밍 언어 이론에서는 이 방식을 명목적 타입 지정(nominal typing)이라고 한다.
구조적(structure)이란 언어의 타입 시스템에서 타입을 구조로 구분한다는 의미다. 이는 타입을 이름으로 구분하는 명목적 타입 지정과 대조를 이룬다.
p.157 특정 기준에 맞는 파일을 처리하는 메서드를 작성할 때 의도를 나타내는 java.io.FileFilter 클래스와 Predicate<File> 중 후자의 사용이 권장된다. 그 이유는 잘 모르겠다. 생각해봐야한다. 하지만 FileFilter 인스턴스를 만들어 내는 메서드가 많은 상황이라면 전자를 사용하자.
p.158 에는 기본 타입 int, long, double에 특화된 함수형 인터페이스가 있으며 이를 사용하면 오토박싱을 줄일 수 있다.
종종 표준 함수형 인터페이스가 적잡하지 않은 상황이 있을 것이다. 이럴땐 인터페이스를 직접 구현해야 한다.
이미지를 색채 패턴으로 채운다고 하고, 각 픽셀에 쓸 색을 만들어 주는 함수를 전달해야한다. (int, int) > Color 매핑에 해당하는 표준 타입은 없다. BiFunction<Integer, Integer, Color>를 사용해도 되지만, 그러면 오토박싱이 일어난다. 이런 상황에선 새 인터페이스를 정의하는 것이 더 좋다.
public interface PixelFunction {
Color apply(int x, int y);
}
public class Image {
BufferedImage create(int width, int height, PixelFunction f) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++) {
Color color = f.apply(x, y);
image.setRGB(x, y, color.getRGB());
}
return image;
}
}
Image image = new Image();
BufferedImage frenchFlag = image.create(150, 100,
(x, y) -> x < 50 ? Color.BLUE : x < 100 ? Color.WHITE : Color.RED);
variable shadowing 이란 로컬 변수와 인스턴스 변수가 같은 이름으로 선언되어져 있을 경우, 로컬 변수를 사용한다.
람다 표현식 바디의 유효 범위는 중첩 블록의 범위와 같기에 이름 충돌(name conflicts) 규칙과 이름 가리기(shadowing) 규칙이 똑같이 적용된다. 그러므로 람다 안에 지역 변수와 이름이 같은 매개변수나 다른 지역 변수를 선언하는 것은 규칙에 어긋난다.
int first = 0;
Comparator<String> comp3 = (first, second) -> first.length() - second.length(); //first 변수는 이미 정의했기 때문에 에러난다.
메서드 안에 이름이 같은 두 지역 변수를 둘 수 없다. 따라서 람다 표현식에도 이름이 같은 지역 변수를 두 개 둘 수 없다.
같은 유효 범위 규칙의 또 다른 결과로 람다 표현식 안에 있는 this 키워드는 해당 람다 표현식을 생성하는 메서드의 this 매개변수를 의미한다. this는 Runnable 인스턴스가 아니라 Application 객체의 toString 메서드를 호출한다.
public class Application {
public void doWork() {
Runnable runner = () -> { System.out.println(this.toString());};
runner.run();
}
}
밑의 코드는 람다 표현식에서 자신을 감싼 메서드나 클래스에 속한 변수에 접근할때이다.
public static void repeatMessage(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
}
};
new Thread(r).start();
}
위의 코드는 repeatMessage 호출이 반환되어 매개변수가 사라지고 나서 한참 뒤에 실행할지도 모른다. 그럼에도 불구하고 text와 count가 표현식이 실행될 시점까지 남아 있는 이유는 위의 람다 표현식이 자유 변수를 사용하고 있기 때문이다. 람다 표현식을 표현하는 자료 구조는 반드시 이런 자유 변수의 값을 저장해야한다. 이를 가리켜 람다 표현식이 이 값들을 캡처(capture) 했다고 한다. 자유 변수의 값을 사용하는 코드 블록을 전문 용어로 클로저(closure)라고 한다.
람다 표현식은 다음 세가지로 구성된다.
캡처된 값이 잘 정의되게 보장하는 중요한 제약이 있다. 람다 표현식에는 값이 변하지 않는 변수만 참조할 수 있다. 그래서 람다 표현식은 변수가 아니라 값을 캡처한다고 말하는 것이다. 밑의 코드에서 i 는 캡처할 수 없다.
for (int i = 0;i < n; i++) {
new Thread(() -> System.out.println(i)).start(); //i를 캡처할 수 없다.
}
람다 표현식은 자신을 감싸는 유효 범위에 속한 사실상 최종(effectively final) 지역 변수에만 접근할 수 있다. 사실상 최종 변수는 절대로 변경되지 않기 때문이다.(이미 final로 선언 했거나 final로 선언이 가능해서 최종 변수라고 한다.)
향상된 for 루프의 변수는 유효 범위가 단일 반복(single iteration)이므로 사실상 최종이다.
String[] args2 = {"a","b","c","d","e"};
for( String arg : args2) {
new Thread(() -> System.out.println(arg)).start();
}
사실상 최종 규칙 때문에 람다 표현식은 캡처한 변수를 어느 것도 변경할 수 없다.
public static void repeatMessage(String text, int count, int threads) {
Runnable r = () -> {
while(count > 0) {
count --; //오류 캡처한 변수를 변경할 수 없음.
System.out.println(text);
}
};
for (int i = 0; i < threads; i++) new Thread(r).start();
}
컴파일러가 모든 병행 접근 오류를 잡아내는 것은 아니다. 캡처한 변수를 변경할 수 없단는 규칙은 변수에만 해당된다. count가 외부 클래스의 인스턴스 변수나 정적 변수라면 결과가 정의되지 않더라도 아무런 오류도 보고하지 않는다.
class Data {
int a = 10;
}
public static void repearMessage(String text, final Data count, int threads) {
Runnable r = () -> {
while(count.a > 0) {
count.a --; //오류 캡처한 변수를 변경할 수 없음.
System.out.println(count.a);
System.out.println(text);
}
};
for (int i = 0; i < threads; i++) new Thread(r).start();
}
repearMessage("aa",data,2);
함수형 프로그래밍 언어에서는 함수가 1차 구성원(기본 요소, first class)이다. 따라서 메서드에 숫자를 전달하고 생성하는 메서드를 만들 수 있는 것처럼 함수를 인수와 반환 값으로 사용할 수 있다. 함수를 처리하거나 반환하는 함수를 고차 함수(higher-order function)라고 한다.
문자열의 배열을 오름차순으로 또는 내림차순으로 정렬하는 비교자를 만들어 내는 메서드를 작성할 수 있다. compareInDirection(1)은 오름차순으로 compareInDirection(-1)은 내림차순의 비교자를 반환한다.
public static Comparator<String> compareInDirection(int direction) {
return (x, y) -> direction * x.compareTo(y);
}
Arrays.sort(friends, compareInDirection(-1));
문자열 비교자를 역으로 만드는 메서드를 작성하면 이 발상을 일반화 할 수 있다. 이 메서드는 함수에도 작동한다. 즉, 함수를 인수로 받아 수정된 함수를 반환한다.
public static Comparator<String> reverse(Comparator<String> comp) {
return (x, y) -> comp.compare(y, x);
}
reverse(String::compareToIgnoreCase)
Comparator 인터페이스에는 비교자를 만들어 내는 유용한 고차 함수가 정적 메서드로 많이 정의되저 있다. 그중 comparing 메서드는 T타입을 String 처럼 비교 가능한 타입으로 매핑하는 ‘키 추출(key extractor)’ 함수를 받는다. 비교 대상 객체에 키 추출 함수를 적용한 후 반환 받은 키를 비교한다.
Employee[] employee = {new Employee("jack"),new Employee("peggy"),new Employee("gracyyy")};
Arrays.sort(employee, Comparator.comparing(Person::getName));
비교 대상이 같으면 thenComparing 메서드로 다른 비교자를 연결해 추가로 비교할 수 있다.
Employee[] employee = {new Employee("jack",1),new Employee("jack",3),new Employee("jack",2)};
Arrays.sort(employee, Comparator.comparing(Person::getName).thenComparing(Person::getId));
comparing과 thenComparing 메서드가 추출하는 키에 적용할 비교자를 지정할 수 있다.
Employee[] employee = {new Employee("jack"),new Employee("peggy"),new Employee("gracyyy")};
Arrays.sort(employee, Comparator.comparing(Person::getName, (s, t) -> s.length() - t.length()));
comparing과 thenComparing 메서드에는 둘 다 int, long, double 값의 박싱을 피하는 변형도 있다.
Employee[] employee = {new Employee("jack"),new Employee("peggy"),new Employee("gracyyy")};
Arrays.sort(employee, Comparator.comparingInt(p -> p.getName().length()));
키 함수가 null을 반환할 가능성이 있다면 nullFirst와 nullLast 어댑터를 사용한다. 이것은 null을 만나도 예외를 던지지 않고 null값을 정상값보다 작은 값과 큰 값으로 취급하도록 비교자를 수정한다. nullFirst 메서드는 비교자를 받는다. naturalOrder메서드는 Comparable을 구현한 모든 클래스에 비교자를 생성한다. reverseOrder는 자연순서의 역으로 비교하는 비교자를 반환한다.
Employee[] employee = {new Employee(null,1) ,new Employee("jack",35),new Employee("jack",3),new Employee("jack",2), new Employee(null,22), new Employee(null,15)};
Arrays.sort(employee, Comparator.comparing(Person::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
람다 표현식이 생기기전에는 인터페이스 하나를 구현하는 클래스를 간결하게 정의하는 메커니즘이 있었다. 함수형 인터페이스라면 람다 표현식을 사용 하겠지만, 때로는 함수형이 아닌 인터페이스를 간결하게 구현하고 싶을 수도 있을 수 있다. 레거시 코드에서도 이런 구조를 접할 수 있다.
메서드 안에도 클래스를 정의할 수 있다. 이렇게 메서드 안에 정의된 클래스를 지역 클래스(local class)라고 한다. 흔히 어떤 클래스가 인터페이스 하나를 구현하고, 메서드를 호출하는 쪽에서 구현 클래스가 아니라 인터페이스에만 관심이 있을 때 전략적으로 지역 클래스를 사용한다. 지역 클래스는 메서드 바깥에서 접근할 수 없으므로 public이나 private으로 선언할 수 없다.
private static Random generator = new Randmo();
public static IntSequence randomInts(int low, int high) {
class RandomSequence implements IntSequence {
public int next() { return low + generator.nextInt(high - low + 1);}
public boolean hasNext() { return true;}
}
return new RandomSequence();
}
클래스를 지역 클래스로 만들면 두가지 이점이 있다.
다음 표현식은 인터페이스와 인터페이스의 메소드를 구현하는 클래스를 정의하고, 이 클래스의 하나뿐인 객체를 생성한다. new interface() { 메서드 구현}
public static IntSequence randomInts(int low, int high) {
return new IntSequence() {
public int next() { return low + generator.nextInt(high - low + 1); }
public boolean hasNext() { return true; }
}
}
람다 표현식이 있는 요즘에는 앞의 예제처럼 메서드를 두 개 이상 제공해야 할 때만 익명 클래스가 필요하다.